#!/usr/bin/env python 
# -*- coding: utf-8 -*-
# @Time    : 2023/4/3 17:10
# @Author  : WJQ
# @Site    : 
# @File    : Graph.py
# @Software: PyCharm
import random
from typing import List, Optional, Dict
from appium.webdriver.webelement import WebElement

import conf
from models.Edge import Edge, CrawlEdge
from models.Node import Node, Component
from utils.common import CrawlerLogger

class Graph:
    nodes: List[Node]
    edges: List[Edge]
    page_id_counter: int
    pages: Dict

    def __init__(self):
        self.nodes = []
        self.edges = []
        self.page_id_counter = 0

    def __repr__(self):
        res = "---GRAPH---\n"
        for n in self.nodes:
            res += str(n) + "\n"
        res += "\n"
        for edge in self.edges:
            res += str(edge.n1) + " -(" + str(edge.value) + "[" + str(edge.visited) + "])-> " + str(edge.n2) + "\n"
        res += "\n---/GRAPH---"
        return res

    def get_page_id( self, path: str, query: str, components: List[Component] ) -> int:
        new_node = Node(path, query, components)
        tmp_list = self.get_node_by_path(path)
        if len(tmp_list) >= 3 and len(components) == 0:
            return random.choice(tmp_list).node_id
        existing_node_id = next((node.node_id for node in tmp_list if Node.are_nodes_similar(new_node, node)), None)

        if existing_node_id:
            return existing_node_id

        self.page_id_counter += 1
        return self.page_id_counter

    def add( self, path: str, query: str, components: List[Component] ):
        node_id = self.get_page_id(path, query, components)
        if node_id == (len(self.nodes) + 1):
            CrawlerLogger.info(f"[+] graph add node {path} node_id is {node_id}.")
            node = Node(path, query, components, node_id)
            self.nodes.append(node)
            return node, False
        else:
            CrawlerLogger.info(f"[+] the {path} has been crawled, update...")
            node = graph.get_node_by_node_id(node_id)
            # node.repeat_number += 1
            # if node.repeat_number > 5:
            #     node.visited = True
            # node = self.update(node_id, components)
            return node, True

    def connect(self, node1: Node,
                    node2: Node,
                    value: CrawlEdge, parent: Optional[Edge] = None):

        n1 = node1
        n2 = node2
        edge = Edge(n1, n2, value, parent)
        n1.children.append(n2)
        n2.parent = n1

        p1 = n1 in self.nodes
        p2 = n2 in self.nodes
        p3 = not (edge in self.edges)
        # print(p1,p2,p3)
        if p1 and p2 and p3:
            CrawlerLogger.info(f"[+] Connected edge: {n1.path} - {value.element.label} > {n2.path}")
            self.edges.append(edge)
            return True
        CrawlerLogger.debug("[-] Failed to connect edge, %s (%s %s %s)" % (str(value), p1, p2, p3))
        return False

    def update(self, node_id: int, components: List[WebElement]):
        old_node = self.get_node_by_node_id(node_id)
        for component in components:
            tmp = Component(component)
            if tmp not in old_node.components:
                CrawlerLogger.debug(f"[-] Updating node {tmp} with component" )
                old_node.components.append(tmp)
        return old_node

    def set_node_visited_by_node_id(self, node_id: int):
        node = self.get_node_by_node_id(node_id)
        if node is not None:
            node.visited = True

    def set_node_state_fail_by_node_id(self, node_id: int):
        node = self.get_node_by_node_id(node_id)
        if node is not None:
            node.state = False

    def get_root_node(self):
        return self.nodes[0]

    def get_node_by_path( self, path: str ) -> List[Node]:
        return [node for node in self.nodes if node.path == path]

    def get_node_by_node_id(self, node_id: int):
        return next((node for node in self.nodes if node.node_id == node_id), None)

    def get_all_parent_node(self, node_id: int):
        node = self.get_node_by_node_id(node_id)
        if node is None:
            return []
        res = []
        while node.parent is not None:
            res.append(node.parent)
            node = node.parent
        return res

    def get_edge_by_node(self, node1: Node, node2: Node):
        return next((edge for edge in self.edges if edge.n1 == node1 and edge.n2 == node2), None)

    def get_all_click_element(self):
        return sum(len(node.components) for node in graph.nodes)

    def get_node_id_and_path( self, driver_handler, components: List[Component] ):
        path, query = driver_handler.get_url()
        tmp_list = self.get_node_by_path(path)
        if not tmp_list:
            return None, None
        else:
            new_node = Node(path, query, components)
            existing_node_id = next((node.node_id for node in tmp_list if Node.are_nodes_similar(new_node, node, 0.8)), random.choice(tmp_list).node_id)
            return existing_node_id, path

    def get_click_edge_list( self, current_node_id: int, target_node_id: int) -> List[Component]:
        res = []
        flag = 0
        tmp_node = self.get_node_by_node_id(target_node_id)
        while tmp_node.node_id != current_node_id and tmp_node.parent.path == tmp_node.path:
            edge = self.get_edge_by_node(tmp_node.parent, tmp_node)
            if edge is None or flag > 4:
                return []
            if edge.value.element.method == "scroll":
                position_dict = {
                    'rect' : {
                        'x' : conf.CLIENT_SIDE_CONF.PHONE_WIDTH / 2,
                        'y' : conf.CLIENT_SIDE_CONF.PHONE_HEIGHT / 2,
                        'width' : 0,  # Add the width key-value pair
                        'height' : 0  # Add the height key-value pair
                    },
                    'text' : "scroll",
                    'tagName' : "",
                    "className" : ""
                }
                res.append(Component(position_dict, "re-scroll"))
            else:
                res.append(edge.value.element)
            tmp_node = tmp_node.parent
            flag += 1
        return res

graph: Graph = Graph()